package com.ppl.server.modules;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.AbstractModule;
import com.google.inject.MembersInjector;
import com.google.inject.ProvisionException;
import com.google.inject.TypeLiteral;
import com.google.inject.matcher.Matchers;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Module that inject members annotated with the {@link com.ppl.server.boot.config.Config} annotation,
 * using the provided Akka {@link com.typesafe.config.Config}. Note that the
 * annotated members should not be annotated with the {@link com.google.inject.Inject} annotation
 * as the config parameters are not bound to the injector, they are resolved
 * on-demand'
 * 
 * @author Nicolas Berthet
 * @since May 3, 2012 10:23:46 AM
 * @see 'http://code.google.com/p/google-guice/wiki/CustomInjections '
 */
public class ConfigModule extends AbstractModule {
	
	private static Logger log = LoggerFactory.getLogger(ConfigModule.class);

	private com.typesafe.config.Config configuration;
	
	private Map<Class<?>, Map<Field, Object>> cache = Maps.newConcurrentMap();
	
	public ConfigModule(com.typesafe.config.Config configuration) {	
		this.configuration = configuration;
	}

	@Override
	protected void configure() {
		// The listener is invoked only once per type, that's where the heavy
		// things are going on.
		binder().bindListener(Matchers.any(), new TypeListener() {			
			@Override
			public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
				
				if (isSuitableForInjection(type.getRawType())) {
					log.info("Type '{}' will be prepared for injection", type);
					encounter.register(new ConfigMembersInjector<I>());
				}
			}
		});
	}
	
	/**
	 * @param targetClass
	 * @return <code>true</code> whether the given class or an ancestor has an
	 *         annotated member.
	 */
	private <I> boolean isSuitableForInjection(Class<I> targetClass) {
		
		for (Class<?> klass = targetClass; klass != null && klass != Object.class; klass = klass.getSuperclass()) {		
			if (klass.isAnnotationPresent(com.ppl.server.boot.config.Config.class)) {
				return true;
			}
			
			for (Field f : klass.getDeclaredFields()) {
				if (f.isAnnotationPresent(com.ppl.server.boot.config.Config.class)) {
					return true;
				}
			}
		}
		
		return false;
	}
	
	private class ConfigMembersInjector<I> implements MembersInjector<I> {
		@Override
		public void injectMembers(I instance) {
			Map<Field, Object> config = cache.get(instance.getClass());
			
			if (config == null) {
				config = createConfiguration(instance.getClass(), configuration);
				cache.put(instance.getClass(), config);
			}
			
			ConfigModule.injectMembers(instance, config);
		}
	}

	public static <I> I configure(@Nonnull Class<I> configType, @Nonnull com.typesafe.config.Config config) {
		return createInstance(configType, createConfiguration(configType, config));
	}

	public static <I> void configure(@Nonnull I configurable, @Nonnull com.typesafe.config.Config config) {
		injectMembers(configurable, createConfiguration(configurable.getClass(), config));
	}

	public static <V> Map<String, V> getMap(@Nonnull Class<V> valueType, @Nonnull String path, @Nonnull com.typesafe.config.Config config) {
		return getMap(config, path, String.class, valueType);
	}

	public static <K, V> Map<K, V> getMap(@Nonnull Class<K> keyType, @Nonnull Class<V> valueType, @Nonnull String path, @Nonnull com.typesafe.config.Config config) {
		return getMap(config, path, keyType, valueType);
	}
	

	private static <I> void injectMembers(I instance, Map<Field, Object> config) {
		if (config == null || config.isEmpty()) {
			return;
		}
		
		for (Map.Entry<Field, Object> e : config.entrySet()) {
			Field f = e.getKey();
			Object value = e.getValue();
			
			try {					
				f.set(instance, value);
			} catch (Exception ex) {
				throw new ProvisionException("Unable to set value '" + value + "' for " + f.getName(), ex);
			}
		}
	}
	
	private static <I> I createInstance(Class<I> clazz, Map<Field, Object> config) {
		I instance;
		try {
			instance = clazz.newInstance();
		} catch (Exception e) {
			throw new ProvisionException("Unable to create configuration pojo instance", e);
		}
		
		injectMembers(instance, config);
		
		return instance;
	}
	
	private static Map<Field, Object> createConfiguration(Class<?> targetClass, com.typesafe.config.Config context) {
				
		// Class level annotation is used to give a prefix (parent path)
		com.ppl.server.boot.config.Config classConfig = targetClass.getAnnotation(com.ppl.server.boot.config.Config.class);
		if (classConfig != null) {
			context = context.getConfig(classConfig.value());
		}		
		
		Map<Field, Object> configurables = Maps.newHashMap();
		for (Field f : getAnnotatedFields(targetClass)) {
			com.ppl.server.boot.config.Config config = f.getAnnotation(com.ppl.server.boot.config.Config.class);
						
			try {
				f.setAccessible(true);
			} catch (Exception e) {
				throw new ProvisionException("Unable to set value '" + config.value() + "' for " + f.getName(), e);
			}
			
			Object value = null;
			
			if (config.optional() && !context.hasPath(config.value())) {
				log.debug("{} no value provided, ignoring optional parameter", config.value());
				// We need to avoid creating a mapping to null to avoid setting
				// primitive types to null.
				continue;
			} else if (isConfig(f.getType())) {
				value = context.getConfig(config.value());
			} else if (isSimpleType(f.getType())) {
				value = context.getAnyRef(config.value());
			} else if (isEnum(f.getType())) {
				value = convert(context.getString(config.value()), f.getType());
			} else if (f.getType() == Class.class) {
				value = convert(context.getString(config.value()), f.getType());
			} else if (isList(f.getType())) {
				Class<?> valueType = getGenericParamType(f, 0);	
					
				value = getList(context, config.value(), valueType);
			} else if (isMap(f.getType())) {
				Class<?> keyType = getGenericParamType(f, 0);
				Class<?> valueType = getGenericParamType(f, 1);

				value = getMap(context, config.value(), keyType, valueType);
			} else {
				// Pojo type ?
				value = createInstance(f.getType(), createConfiguration(f.getType(), context.getConfig(config.value())));
			}

			// TODO [nbe] Support basic conversions (list -> array, int -> long, ...)
			// TODO [nbe] Support for pojo collections
			
			configurables.put(f, value);
		}
		
		
		if (configurables.isEmpty()) {
			configurables = Collections.emptyMap();
		}
		
		return configurables;
	}
	
	@SuppressWarnings("unchecked")
	private static <V> List<V> getList(com.typesafe.config.Config context, String path, Class<V> valueType) {
		if (isSimpleType(valueType)) {
			return (List<V>) context.getAnyRef(path);
		} else {						
			List<? extends com.typesafe.config.Config> list = context.getConfigList(path);
			
			// Create a list of object, with erasure, it won't matter, just to avoid a warning
			List<V> values = Lists.newArrayListWithExpectedSize(list.size());
			for (com.typesafe.config.Config conf : list) {
				values.add(createInstance(valueType, createConfiguration(valueType, conf)));
			}
			
			return values;
		}
	}
	
	private static <K, V> Map<K, V> getMap(com.typesafe.config.Config context, String path, Class<K> keyType, Class<V> valueType) {
		com.typesafe.config.ConfigObject object = context.getObject(path);
		
		Map<K, V> values = Maps.newHashMap();
		for (String key : object.keySet()) {
			values.put(convert(key, keyType), getValue(object.toConfig(), key, valueType));
		}
		
		return values;
	}
	
	private static <V> V getValue(com.typesafe.config.Config context, String path, Class<V> valueType) {
		if (isSimpleType(valueType)) {
			return valueType.cast(context.getAnyRef(path));
		} else {
			return createInstance(valueType, createConfiguration(valueType, context.getConfig(path)));
		}
	}
	
	private static List<Field> getAnnotatedFields(Class<?> clazz) {
		
		List<Field> fields = Lists.newLinkedList();

		for (Class<?> klass = clazz; klass != null && klass != Object.class; klass = klass.getSuperclass()) {			
			for (Field f : klass.getDeclaredFields()) {			
				com.ppl.server.boot.config.Config config = f.getAnnotation(com.ppl.server.boot.config.Config.class);
				if (config != null) {
					fields.add(f);
				}
			}
		}
		
		return fields;
	}
	
	private static boolean isConfig(Class<?> type) {
		return type.isAssignableFrom(com.typesafe.config.Config.class);
	}
	
	private static boolean isSimpleType(Class<?> type) {
		// TODO [nbe] Should add all the Boxed type
		return type.isPrimitive()
				|| type.equals(Integer.class)
				|| type.equals(String.class);
	}
	
	private static boolean isList(Class<?> type) {
		return type.isAssignableFrom(List.class);
	}
	
	private static boolean isMap(Class<?> type) {
		return type.isAssignableFrom(Map.class);
	}
	
	private static boolean isEnum(Class<?> type) {
		return type.isEnum();
	}
	
	private static <T> T convert(String value, Class<T> targetType) {
		try {
			if (targetType.isAssignableFrom(String.class)) {
				return targetType.cast(value);
			} else if (targetType.isAssignableFrom(Integer.class)) {
				return targetType.cast(Integer.parseInt(value));
			} else if (targetType.isAssignableFrom(Boolean.class)) {
				return targetType.cast(Boolean.parseBoolean(value));
			} else if (isEnum(targetType)) {
				return getEnumValue(targetType, value);
			} else if (targetType.isAssignableFrom(Class.class)) {
				return targetType.cast(Class.forName(value));
			}
		} catch (Exception e) {
			throw new ProvisionException("Unable to convert '" + value + "' in an instance of " + targetType.getName(), e);	
		}
		throw new ProvisionException("Unable to convert '" + value + "' in an instance of " + targetType.getName());
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static <T> T getEnumValue(Class<T> enumType, String value) {
		return (T) Enum.valueOf( (Class) enumType, value);
	}
	
	// Simplistic version of reality, but far enough to cover for our usage patterns.
	private static Class<?> getGenericParamType(Field f, int index) {
		ParameterizedType pType = (ParameterizedType) f.getGenericType();
		// Let's crash if the type of collection is not supported on unparametrized
		Type type = pType.getActualTypeArguments()[index];
		if (type instanceof Class) {
			return (Class<?>) type;
		}
		if (type instanceof ParameterizedType) {
			return (Class<?>) ((ParameterizedType) type).getRawType();
		}
		
		throw new ProvisionException("Unable to determine the type of generic field '" + f.getName() + "'");
	}
	
	/**
	 * Describe the given class's configuration by outputing all its values in
	 * the given {@link java.io.PrintStream}.
	 */
	public static void describe(Class<?> configurableClass, PrintStream out) {
		describe(configurableClass, 0, out);
	}
	
	private static void describe(Class<?> targetClass, int level, PrintStream out) {
		
		String padTop = Strings.repeat("\t", level);
		
		com.ppl.server.boot.config.Config root = targetClass.getAnnotation(com.ppl.server.boot.config.Config.class);
		if (root != null) {
			out.println(padTop + root.value() + " {");
			level++;
		}
		
		String pad = Strings.repeat("\t", level);
		
		for (Field f : getAnnotatedFields(targetClass)) {
			com.ppl.server.boot.config.Config config = f.getAnnotation(com.ppl.server.boot.config.Config.class);
			
			if (!Strings.isNullOrEmpty(config.description())) {
				out.println(pad + "# " + config.description());
			}
			
			if (isConfig(f.getType())) {
				out.println(pad + config.value() + " {");
				out.println(pad + "// This block may contain unknown configuration");
				out.println(pad + "}");
			} else if (isSimpleType(f.getType())) {				
				out.println(pad + config.value() + " = // " + f.getType().getSimpleName());
			} else if (isEnum(f.getType())) {
				out.print(pad + config.value() + " = // Enum : ");
				int index = 0;
				for (Object enumValue : f.getType().getEnumConstants()) {
					if (index++ > 0) {
						out.print(", ");
					}
					out.print(enumValue);
				}
				out.println("");
			} else if (isList(f.getType())) {
				out.println(pad + config.value() + " = [ // This may contain zero or more elements");
				Class<?> innerType = getGenericParamType(f, 0);
				if (isSimpleType(innerType)) {
					out.println(pad + "\t// " + innerType.getSimpleName());
				} else {
					out.println(pad + "\t{");
					describe(innerType, level + 2, out);
					out.println(pad + "\t}");
				}
				out.println(pad + "]");
			} else {
				out.println(pad + config.value() + " {");
				describe(f.getType(), level + 1, out);
				out.println(pad + "}");
			}
		}
		
		if (root != null) {
			level--;
			out.println(padTop + "}");
		}
	}
}
